package edu.hawaii.senin.iclouds.simple;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.Formatter;
import java.util.Iterator;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.Random;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import java.util.logging.XMLFormatter;

import org.apache.commons.math.random.RandomAdaptor;
import org.apache.commons.math.random.RandomGenerator;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;

import edu.hawaii.senin.iclouds.basics.Accumulator;
import edu.hawaii.senin.iclouds.basics.Atom;
import edu.hawaii.senin.iclouds.basics.Constants;
import edu.hawaii.senin.iclouds.basics.Interval;
import edu.hawaii.senin.iclouds.basics.IntervalComparator;
import edu.hawaii.senin.iclouds.basics.LongAccumulator;
import edu.hawaii.senin.iclouds.basics.Molecule;
import edu.hawaii.senin.iclouds.basics.PopulationException;
import edu.hawaii.senin.iclouds.basics.PopulationSimple;
import edu.hawaii.senin.iclouds.basics.PopulationWithBury;
import edu.hawaii.senin.iclouds.basics.Reaction;
import edu.hawaii.senin.iclouds.basics.ReadConfigException;
import edu.hawaii.senin.iclouds.basics.RngPackGenerator;
import edu.hawaii.senin.iclouds.basics.Specie;
import edu.hawaii.senin.iclouds.controller.Controller;

/**
 * This class represents simple model of interstellar cloud.
 *
 * @author Pavel Senin
 *
 */
public class Cloud
    extends Observable implements Runnable, Observer {

  private static final String newLine = "\n";

  private static final Integer zeroValue = 0;
  /**
   * How many species accreted at once.
   */
  private Integer accretionStep;

  /**
   * Number of active sites.
   */
  private Long activeSites;

  /**
   * Switches bury routine on/off.
   */
  private Boolean buryIsOn;

  private final Calendar calendar = Calendar.getInstance();

  /**
   * Specifies accretion time.
   */
  private Double cloudAccretionTimeSpan;

  /**
   * Specifies density within this cloud.
   */
  private Double cloudDensity;

  /**
   * Barrier width.
   */
  private Double cloudGeneralBarrierWidth;

  /**
   * Grain size.
   */
  private Double cloudGrainSize = 1000.0;

  private Logger cloudLogger = Logger.getLogger("edu.hawaii.senin.iclouds.simple.cloud");

  /**
   * Contains all atoms and molecules within the cloud along with abundances.
   */
  private PopulationSimple cloudPopulation;

  /**
   * Contains all specified reactions for that gaseous phase along with rates.
   */
  private TreeMap<String, Reaction> cloudReactions;

  /**
   * Specifies temperature within this cloud.
   */
  private Double cloudTemperature;

  /**
   * specifies initial cloud configuration.
   */
  private String configFileName;

  private TreeMap<String, LongAccumulator> coreactantPickingStorage;

  private final DecimalFormat decFormat = new DecimalFormat("#####0");

  private final DecimalFormat decFormatDot = new DecimalFormat("###0.0000");

  private final DecimalFormat decFormatE = new DecimalFormat("0.0E00");

  private Double DHRatio;

  private boolean isReadyToRun;

  private boolean isStopSimulation;

  /**
   * Counts iterations.
   */
  private Integer iterationCounter;

  /**
   * Limit of iterations, will exit from main loop if even not converge yet.
   */
  private Integer iterationsLimit;
  /**
   * Switches log output format.
   */
  private String logFormat;
  private String loggerName;
  private Level logLevel;

  private BufferedWriter logWriter;
  /**
   * Random number generator handler.
   */
  private Random random;
  /**
   * Random number generator wrapper for RanMT.
   */
  private RandomGenerator randomGenerator;

  /**
   * Seed for random generator.
   */
  private Integer randomSeed;

  private TreeMap<String, LongAccumulator> reactantPickingStorage;

  private TreeMap<String, TreeMap<String, LongAccumulator>> reactionLoggerStorage;

  TreeMap<String, LongAccumulator> summaryLog;

  /**
   * Contains almost all atoms and molecules within the grain surface along with their abundances.
   */
  private PopulationWithBury surfacePopulation;

  /**
   * Contains surface HD and D so far.
   */
  private PopulationSimple surfacePopulationHelper;

  private Document XMLConfiguration;

  private String logName;

  private Boolean cascadeIsOn;

  /**
   * Reports current cloud population.
   *
   * @return cloud population.
   */
  public PopulationSimple getCloudPopulation() {
    return this.cloudPopulation;
  }

  /**
   * Reports cloud reactions.
   *
   * @return cloud reactions network.
   */
  public CloudReactions getcloudReactions() {
    CloudReactions rStore = new CloudReactions();
    for (Reaction r : this.cloudReactions.values()) {
      try {
        rStore.add(r);
      }
      catch (CloudReactionsException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }
    return rStore;
  }

  /**
   * Reports Population statistics.
   *
   * @param storage storage used for population.
   * @param str the title to be used for statistics.
   * @return the statistics.
   */
  private StringBuilder getCPStatistics(TreeMap<String, LongAccumulator> storage, String str) {
    StringBuilder sb = new StringBuilder(5000);
    for (String sp : storage.keySet()) {
      sb.append(sp + "," + storage.get(sp).getValue() + ",");
    }
    sb.delete(sb.length() - 1, sb.length());
    sb.insert(0, str + ",");
    sb.append(newLine);
    return sb;
  }

  /**
   * Reports current grain population.
   *
   * @return grain population.
   */
  public PopulationWithBury getGrainPopulation() {
    return this.surfacePopulation;
  }

  /**
   * Reports current grain helper population.
   *
   * @return grain helper population.
   */
  public PopulationSimple getGrainPopulationHelper() {
    return this.surfacePopulationHelper;
  }

  /**
   * Gets hit with random number within the intervals provided.
   *
   * @param rnd the random value.
   * @param intervals the intervals collection.
   * @return the id of the interval that got hit.
   */
  private String getHit(Double rnd, Interval[] intervals) {
    int lowIdx = 0;
    int highIdx = intervals.length;
    int curIdx = (highIdx - lowIdx) / 2;
    while (!intervals[curIdx].within(rnd)) {
      if (rnd < intervals[curIdx].getLowBound()) {
        highIdx = curIdx;
      }
      else {
        lowIdx = curIdx;
      }
      curIdx = lowIdx + (highIdx - lowIdx) / 2;
    }
    return intervals[curIdx].getName();
  }

  /**
   * Reports possible reactions list for the certain specie.
   *
   * @param choosenBeing the query specie.
   * @param possibleReactions the map to use. MAP SHOULD BE INSTANTIATED BEFORE.
   * @return the same map but with new content.
   */
  TreeMap<String, Reaction> getPossibleReactions(String choosenBeing,
      TreeMap<String, Reaction> possibleReactions) {
    possibleReactions.clear();
    for (Reaction reaction : this.cloudReactions.values()) {
      ArrayList<String> reactants = reaction.getReactants();
      String reactant1 = reactants.get(0);
      String reactant2 = reactants.get(1);
      if ((choosenBeing.equalsIgnoreCase(reactant1))
          && (this.surfacePopulation.surfaceContains(reactant2))) {
        possibleReactions.put(reactant2, reaction);
      }
    }
    return possibleReactions;
  }

  /**
   * Extracts reaction products from the list.
   *
   * @param products list of products to extract from.
   * @return the text-element list.
   */
  private ArrayList<String> getProducts(List products) {

    ArrayList<String> pList = new ArrayList<String>();
    Iterator itr = products.iterator();
    while (itr.hasNext()) {
      pList.add(((Element) itr.next()).getValue());
    }
    return pList;
  }

  /**
   * Extracts reactants from the list.
   *
   * @param reactants list to extract from.
   * @return the text-element list.
   */
  private ArrayList<String> getReactants(List reactants) {

    ArrayList<String> rList = new ArrayList<String>();
    Iterator itr = reactants.iterator();
    while (itr.hasNext()) {
      rList.add(((Element) itr.next()).getValue());
    }

    return rList;
  }

  /**
   * Prints nice looking coreactant choice statistics into string buffer.
   *
   * @param reactionsLog map containing reaction log.
   * @return the statistics.
   */
  private StringBuilder getStatistics(TreeMap<String, TreeMap<String, LongAccumulator>> reactionsLog) {
    StringBuilder sb = new StringBuilder(5000);
    Integer reactNum = 0;
    ArrayList<String> species = new ArrayList<String>();
    for (TreeMap<String, LongAccumulator> m : reactionLoggerStorage.values()) {
      for (String st : m.keySet()) {
        if (species.contains(st)) {
          assert true;
        }
        else {
          species.add(st);
        }
      }
    }
    Collections.sort(species);
    sb.append(species.toString().replace(", ", ","));
    sb.delete(0, 1);
    sb.delete(sb.length() - 1, sb.length());
    sb.insert(0, "species,");
    sb.append(newLine);

    reactNum = species.size();
    for (String str : reactionLoggerStorage.keySet()) {
      sb.append(str + ",");
      TreeMap<String, LongAccumulator> map = reactionLoggerStorage.get(str);
      Integer ctr = 0;
      while (ctr < reactNum) {
        String sp = species.get(ctr);
        Long value = 0L;
        if (map.containsKey(sp)) {
          value = map.get(sp).getValue();
        }
        // sb.append(sp + "," + value + ",");
        sb.append(value + ",");
        ctr++;
      }
      sb.delete(sb.length() - 1, sb.length());
      sb.append(newLine);
    }
    return sb;
  }

  /**
   * Helper to get interval in Sampling on the reaction network.
   *
   * @param coreactants all possible coreactants with their reactions.
   * @return sorted intervals ready for sampling.
   */
  Interval[] getSurfaceIntervals(TreeMap<String, Reaction> coreactants) {

    TreeSet<Interval> res = new TreeSet<Interval>(new IntervalComparator());

    Double sum = 0D;
    for (String s : coreactants.keySet()) {
      sum += this.surfacePopulation.surfaceGetSpecie(s).getAccumulatorValue().doubleValue()
          * coreactants.get(s).getChi();
    }

    Double low = 0D;
    for (String s : coreactants.keySet()) {
      Double high = low
          + (this.surfacePopulation.surfaceGetSpecie(s).getAccumulatorValue().doubleValue() * coreactants
              .get(s).getChi()) / sum;
      res.add(new Interval(s, low, high));
      low = high;
    }

    // **************** do a little logging
    StringBuilder sb = new StringBuilder(2000);
    sb.append("got H to react: ");
    for (Interval intr : res) {
      sb.append(intr.getName() + " (" + intr.print() + ") surfCon "
          + this.surfacePopulation.surfaceGetSpecie(intr.getName()).getAccumulatorValue() + ", ");
    }
    this.cloudLogger.finer(sb.toString());
    // ****************

    Interval[] resArray = new Interval[res.size()];
    return res.toArray(resArray);
  }

  /**
   * Helper method that calculates time span between two events and converts it to the human
   * readable string format.
   *
   * @param startTime first event time.
   * @param endTime second event time.
   * @return time span.
   */
  private String getTimeSpan(Long startTime, Long endTime) {
    Long delta = endTime - startTime;
    Long hours = delta / 3600000;
    Long minutes = (delta % 3600000) / 60000;
    Long seconds = (delta - hours * 3600000 - minutes * 60000) / 1000;
    return hours + "h " + minutes + "m " + seconds + "s";
  }

  /**
   * Instantiates and initializes cloud model. Calculates all needed data from supplied by
   * configuration initial data. Does exactly next moves:
   * <ul>
   * <li>fixes local time and iteration;</li>
   * <li>sets velocities;</li>
   * <li>sets activation barrier width;</li>
   * <li>fixes all reactions accretion rate;</li>
   * </ul>
   *
   */
  public void initialize() {
    // check and set missed parameters
    if (null == this.DHRatio) {
      this.DHRatio = 0D;
    }
    else {
      Atom d = (Atom) this.cloudPopulation.getSpecie("D");
      d.setFraction(this.DHRatio);
    }
    if (null == this.accretionStep) {
      this.accretionStep = 1000;
    }
    if (null == this.iterationsLimit) {
      this.iterationsLimit = 1;
    }
    if (null == this.logName) {
      this.logName = System.getenv("TEMP") + "/iclouds.log";
      ;
    }
    if (null == this.loggerName) {
      this.loggerName = System.getenv("TEMP") + "/iclouds.logger.log";
      ;
    }
    if (null == this.logLevel) {
      this.logLevel = Level.OFF;
    }
    if (null == this.logFormat) {
      this.logFormat = "text";
    }
    if (null == this.cascadeIsOn) {
      this.cascadeIsOn = true;
    }

    // clean up grain population storages
    this.surfacePopulation = new PopulationWithBury(this.activeSites);
    if (null == this.buryIsOn) {
      this.buryIsOn = true;
    }
    this.surfacePopulationHelper = new PopulationSimple();

    // init random generator with the same seed.
    this.randomSeed = ((Long) (System.currentTimeMillis() * 31)).intValue();
    this.randomGenerator = new RngPackGenerator();
    this.random = RandomAdaptor.createAdaptor(this.randomGenerator);
    random.setSeed(this.randomSeed);

    // check and set this iteration counter
    if (null == this.iterationCounter) {
      this.iterationCounter = 0;
    }

    // check and set velocities for species
    for (Specie sp : this.cloudPopulation) {
      sp.setVelocity(Math.sqrt(3 * Constants.boltzmann * this.cloudTemperature
          / (Constants.atomicMassUnit * sp.getAmu())));
    }

    // set-up the barrier width
    this.cloudGeneralBarrierWidth = 1E-10;

    // update Chi energy barriers for reactions
    this.updateChiValues();
  }

  /**
   * List products for pretty logging.
   *
   * @param products products to list.
   * @return the string containing pretty listing.
   */
  private String list(ArrayList<Specie> products) {
    StringBuilder sb = new StringBuilder(800);
    for (Specie sp : products) {
      sb.append(sp.getName() + " ");
    }
    return sb.toString();
  }

  /**
   * Loads xml formatted initial cloud configuration.
   *
   * @throws ReadConfigException if config file is not correctly formatted.
   */
  public void loadConfig() throws ReadConfigException {
    SAXBuilder builder = new SAXBuilder();
    this.XMLConfiguration = null;
    try {
      BufferedReader reader = new BufferedReader(new FileReader(new File(this.configFileName)));
      this.XMLConfiguration = builder.build(reader);
    }
    catch (FileNotFoundException e) {
      setChanged();
      notifyObservers("model : can't really open cloud config file" + this.configFileName + "\n"
          + e.toString());
    }
    catch (JDOMException e) {
      setChanged();
      notifyObservers("model : cloud config file is not well formed " + this.configFileName + "\n"
          + e.toString());
    }
    catch (IOException e) {
      setChanged();
      notifyObservers("model : IO exception while parsing config file  " + this.configFileName
          + "\n" + e.toString());
    }
    Element root = this.XMLConfiguration.getRootElement();
    if (null == root) {
      throw new ReadConfigException("Got no root element in config file");
    }
    else {
      this.cloudTemperature = Double.valueOf(root.getChild("temperature").getValue());
      this.cloudDensity = Double.valueOf(root.getChild("density").getValue());
      this.DHRatio = Double.valueOf(root.getChild("dh_ratio").getValue());
      this.activeSites = Double.valueOf(root.getChild("number_sites").getValue()).longValue();
      this.cloudGrainSize = Double.valueOf(root.getChild("grain_size").getValue());
      this.accretionStep = (Double.valueOf(root.getChild("accretion_step").getValue())).intValue();
      this.iterationsLimit = (Double.valueOf(root.getChild("iteration_limit").getValue()))
          .intValue();

      this.cloudPopulation = new PopulationSimple();
      this.parsePopulation(root.getChild("population").getChild("gasphase").getChildren());

      this.cloudReactions = new TreeMap<String, Reaction>();
      this.parseReactions(root.getChild("reactions").getChildren());
    }
    initialize();
    updateAccretionRates();

    setChanged();
    notifyObservers("model: config " + this.configFileName + " parsed at "
        + this.calendar.getTime());

    setChanged();
    notifyObservers(getCloudParameters());

    setChanged();
    notifyObservers(getcloudReactions());

    setChanged();
    notifyObservers(getCloudPopulation());

    this.isReadyToRun = true;
  }

  /**
   * Writes the message to the log and prints it on the display.
   *
   * @param outMessage the message to print/log.
   */
  private void logMessage(String outMessage) {
    // check for auxiliary logger here, if so, do logging into
    if (null != this.logWriter) {
      try {
        System.out.print(outMessage);
        this.logWriter.write(outMessage);
        this.logWriter.flush();
      }
      catch (IOException e) {
        System.out.print("Got an exception when logging: " + e.getMessage());
      }
    }

  }

  /**
   * Logs the reactions chosen at each step for statistics.
   *
   * @param storage storage to use for logging.
   * @param choosenBeing specie selected to react.
   * @param coreactant the selected coreactant.
   */
  private void logReaction(TreeMap<String, TreeMap<String, LongAccumulator>> storage,
      String choosenBeing, String coreactant) {

    if (null == storage) {
      assert true;
    }
    else {
      if (storage.containsKey(choosenBeing)) {
        assert true;
      }
      else {
        storage.put(choosenBeing, new TreeMap<String, LongAccumulator>());
      }

      TreeMap<String, LongAccumulator> tm = storage.get(choosenBeing);

      if (tm.containsKey(coreactant)) {
        Long oldValue = tm.get(coreactant).getValue();
        tm.get(coreactant).setValue(oldValue + 1);
      }
      else {
        tm.put(coreactant, new LongAccumulator(1L));
      }
    }
  }

  /**
   * Logs the specie picking for reactions network.
   *
   * @param storage log storage.
   * @param coreactant which specie pick to log.
   */
  private void logSpeciePick(TreeMap<String, LongAccumulator> storage, String coreactant) {
    if (null == storage) {
      assert true;
    }
    else {
      if (storage.containsKey(coreactant)) {
        storage.get(coreactant).increment();
      }
      else {
        storage.put(coreactant, new LongAccumulator(1L));
      }
    }
  }

  /**
   * Reports whether or not this cloud is ready for running simulation.
   *
   * @return the readiness.
   */
  public boolean modelReady() {
    return this.isReadyToRun;
  }

  /**
   * Parses population section of the config.
   *
   * @param species JDOM formatted list of all species.
   */
  private void parsePopulation(List species) {
    Iterator itr = species.iterator();
    while (itr.hasNext()) {

      Element el = (Element) itr.next();

      String specieType = el.getName();

      try {

        if ("atom".equalsIgnoreCase(specieType)) {
          Atom a = new Atom(el);
          this.cloudPopulation.addSpecie(a.getName(), a, 1L);
        }
        else if ("molecule".equalsIgnoreCase(specieType)) {
          Molecule m = new Molecule(el);
          this.cloudPopulation.addSpecie(m.getName(), m, 1L);
        }

      }
      catch (PopulationException e) {
        e.printStackTrace();
      }

    }

  }

  /**
   * Parses reactions section of the config.
   *
   * @param react JDOM formatted list of all reactions.
   * @throws ReadConfigException
   */
  private void parseReactions(List react) {
    // <reaction>
    // <name>foo</name>
    // <rate>1.0</rate>
    // <reactants>
    // <atom>H</atom>
    // <atom>C</atom>
    // </reactants>
    // <products>
    // <molecule>CH</molecule>
    // </products>
    // <energy_barrier>105.0</energy_barrier>
    // <type>DF</type>
    // </reaction>
    Iterator itr = react.iterator();
    while (itr.hasNext()) {

      Element el = (Element) itr.next();

      String name = el.getChild("name").getValue();
      String type = el.getChild("type").getValue();
      Double barrier = Double.valueOf(el.getChild("energy_barrier").getValue());

      ArrayList<String> reactants = getReactants(el.getChild("reactants").getChildren());
      ArrayList<String> products = getProducts(el.getChild("products").getChildren());

      try {
        this.cloudReactions.put(name, new Reaction(name, barrier, 1.0, type, reactants, products));
      }
      catch (ReadConfigException e) {
        System.err.println("### there was an error:");
        e.printStackTrace();
      }
    }
  }

  /**
   * Updates intervals for a random choice of reacting species.
   *
   * @param species space where put the map.
   * @param iPopulation the population for which we are building intervals.
   * @return all intervals according current surface population.
   *
   */
  private TreeMap<String, Interval> refreshIntervals(TreeMap<String, Interval> species,
      PopulationSimple iPopulation) {
    species.clear();
    Long totalPopulation = 0L;
    for (Specie sp : iPopulation) {
      totalPopulation += sp.getAccumulatorValue();
    }
    Double currentLow = 0.0;
    for (Specie sp : iPopulation) {
      Double highBound = currentLow + sp.getAccumulatorValue().doubleValue()
          / totalPopulation.doubleValue();
      species.put(sp.getName(), new Interval(sp.getName(), currentLow, highBound));
      currentLow = highBound;
    }
    return species;
  }

  /**
   * Runs the threaded simulation of this cloud.
   */
  public void run() {

    getCloudParameters().print();

    // the spacer to use in logging
    final String spacer = "   ";
    this.isStopSimulation = false;

    // logging storages init section
    this.reactionLoggerStorage = new TreeMap<String, TreeMap<String, LongAccumulator>>();
    this.coreactantPickingStorage = new TreeMap<String, LongAccumulator>();
    this.reactantPickingStorage = new TreeMap<String, LongAccumulator>();
    this.summaryLog = new TreeMap<String, LongAccumulator>();

    try {

      // set up the logger
      this.logWriter = new BufferedWriter(new FileWriter(new File(this.logName)));

      FileHandler fh = new FileHandler(this.loggerName);

      if ("text".equalsIgnoreCase(this.logFormat)) {
        fh.setFormatter(new SimpleFormatter());
      }
      else {
        fh.setFormatter(new XMLFormatter());
      }

      this.cloudLogger.addHandler(fh);
      this.cloudLogger.setLevel(this.logLevel);

      // first collect the cloud setup data into log buffer
      StringBuilder cloudRunIDSLogData = new StringBuilder(1000);
      // Map<String, String> sysInfo = System.getenv();
      cloudRunIDSLogData.append("running at "
          + DateFormat.getDateInstance(DateFormat.FULL).format(System.currentTimeMillis()) + ", "
          + DateFormat.getTimeInstance(DateFormat.FULL).format(System.currentTimeMillis())
          + ", config used: " + this.configFileName);
      StringBuilder cloudSetupLogData = new StringBuilder(1000);
      cloudSetupLogData.append("density " + decFormatE.format(this.cloudDensity) + ", temperature "
          + decFormatE.format(this.cloudTemperature) + ", grain size "
          + decFormatE.format(this.cloudGrainSize) + ", active sites "
          + decFormatE.format(this.activeSites) + ", accretion step "
          + decFormatE.format(this.accretionStep) + ", iteration limit "
          + decFormat.format(this.iterationsLimit));

      // update all computable values
      updateAccretionRates();
      updateChiValues();

      // collect the data of particular interest
      StringBuilder extraLogData = new StringBuilder("D to H ratio "
          + decFormatDot.format(this.DHRatio)
          // .format(this.cloudPopulation.getSpecie("D").getAccretionProbability()
          // / (this.cloudPopulation.getSpecie("D").getAccretionProbability() +
          // this.cloudPopulation.getSpecie("H").getAccretionProbability())) +
          + ", bury routine: " + this.buryIsOn.toString() + ", cascade: "
          + this.cascadeIsOn.toString());
      // collect population and accretion rates data
      StringBuilder populationLogData = new StringBuilder(500);
      for (Specie sp : this.cloudPopulation) {
        populationLogData.append(sp.getName() + ",");
      }
      populationLogData.delete(populationLogData.length() - 1, populationLogData.length());
      StringBuilder accretionRatesLogData = new StringBuilder(500);
      for (Specie sp : this.cloudPopulation) {
        accretionRatesLogData.append(decFormatDot.format(sp.getAccretionProbability()) + " ");
      }
      accretionRatesLogData.delete(accretionRatesLogData.length() - 1, accretionRatesLogData
          .length());

      // dump the log into the system log, on screen and auxiliary log file
      this.cloudLogger.finer("run_setup: " + cloudRunIDSLogData.toString());
      this.cloudLogger.finer("cloud_setup: " + cloudSetupLogData.toString());
      this.cloudLogger.finer("misc_log: " + extraLogData.toString());
      this.cloudLogger.finer("population: " + populationLogData);
      this.cloudLogger.finer("accretion_prob: " + accretionRatesLogData);
      this.cloudLogger.finer("started_simulation:" + new Date(System.currentTimeMillis()));

      logMessage("## current setup:\n" + spacer + cloudRunIDSLogData.toString() + newLine + spacer
          + cloudSetupLogData.toString() + newLine + spacer + extraLogData.toString() + newLine
          + spacer + "population " + populationLogData + newLine + spacer + "rates "
          + accretionRatesLogData + newLine);

      // The reaction ststistics display.
      // StringBuilder sb_reactions = new StringBuilder(5000);
      // for(Reaction r : this.cloudReactions.values()){
      // sb_reactions.append(r.getName() + " : " + this.decFormatE.format(r.getChi()) + "\n");
      // }
      // logMessage("## reactions set up: \n" + sb_reactions.toString() + newLine + spacer +
      // newLine);

      // here we go over iterations
      for (int i = 0; i < this.iterationsLimit; i++) {

        // set up log buffers
        StringBuilder sb1 = new StringBuilder(3000);
        StringBuilder sb2 = new StringBuilder(3000);
        sb1.append("## iteration : " + i + " ( "
            + DateFormat.getDateInstance(DateFormat.FULL).format(System.currentTimeMillis())
            + " , "
            + DateFormat.getTimeInstance(DateFormat.FULL).format(System.currentTimeMillis()) + " )"
            + newLine);
        sb2.append("accretion_iteration " + i);

        // run the accretion and fix the wall clock
        Long startTime = System.currentTimeMillis();
        PopulationSimple acrretionSample = runCycleAccretion();
        Long endTime = System.currentTimeMillis();
        String timeSpan = getTimeSpan(startTime, endTime);

        // dump accretion statistics
        sb1.append(spacer + "model: accretion cycle finished in " + timeSpan + " accreted "
            + decFormatE.format(this.accretionStep) + newLine);
        sb2.append(" finished in " + timeSpan + " accreted "
            + decFormatE.format(this.accretionStep));

        Formatter printFormatter = new Formatter();
        Formatter loggerFormatter = new Formatter();
        for (Specie s : acrretionSample) {
          printFormatter.format("%1$10s, %2$10d, %3$10.4f%4$1%%5$n", s.getName(), s
              .getAccumulatorValue(), Float
              .valueOf((100F * (Float.valueOf(s.getAccumulatorValue()) / this.accretionStep))));
          loggerFormatter.format(" %1$s, %2$10d, %3$7.4f%4$1%", s.getName(), s
              .getAccumulatorValue(), Float
              .valueOf((100F * (Float.valueOf(s.getAccumulatorValue()) / this.accretionStep))));
        }
        sb1.append(printFormatter + spacer + "----" + newLine);
        sb2.append(loggerFormatter);

        // notify observers with accretion joy and log the statistics
        setChanged();
        notifyObservers(sb1.toString());
        logMessage(sb1.toString());
        this.cloudLogger.finer(sb2.toString());

        // clear log buffers
        sb1.delete(0, sb1.length());
        sb2.delete(0, sb2.length());
        printFormatter = new Formatter();
        loggerFormatter = new Formatter();

        // do a surface chemistry cycle
        startTime = System.currentTimeMillis();
        runSurfaceCycle(acrretionSample);
        endTime = System.currentTimeMillis();
        timeSpan = getTimeSpan(startTime, endTime);
        sb1.append(spacer + "model: surface cycle " + i + " finished in " + timeSpan + newLine);
        sb2.append(spacer + "surface_cycle " + i + " finished in " + timeSpan);

        // extract active population statistics
        String logstr = " surface population species: "
            + this.surfacePopulation.surfaceSpeciesNum() + " surface population size: "
            + this.surfacePopulation.surfaceTotalSize() + ", total population: "
            + this.surfacePopulation.totalTotalSize();
        sb1.append(spacer + logstr + newLine);
        sb2.append(logstr);

        Iterator itr = this.surfacePopulation.surfaceGetIterator();
        while (itr.hasNext()) {
          Specie s = (Specie) itr.next();
          printFormatter.format("%1$10s, %2$10d, %3$10.4f%4$1%%5$n", s.getName(), s
              .getAccumulatorValue(), Float
              .valueOf(100F * (Float.valueOf(s.getAccumulatorValue()) / this.surfacePopulation
                  .surfaceTotalSize())));
          loggerFormatter.format(" %1$10s, %2$10E, %3$7.4f%4$1% ", s.getName(), s
              .getAccumulatorValue().doubleValue(), Float.valueOf(100F * (Float.valueOf(s
              .getAccumulatorValue()) / this.surfacePopulation.surfaceTotalSize())));
        }
        sb1.append(printFormatter + spacer + "----" + newLine);
        sb2.append(loggerFormatter);

        // extract buried population statistics
        printFormatter = new Formatter();
        loggerFormatter = new Formatter();
        logstr = "buried population species: " + this.surfacePopulation.buriedSpeciesNum()
            + ", buried population size: " + this.surfacePopulation.buriedTotalSize()
            + ", total population: " + this.surfacePopulation.totalTotalSize();
        sb1.append(spacer + logstr + newLine);
        sb2.append(logstr);

        itr = this.surfacePopulation.buriedGetIterator();
        while (itr.hasNext()) {
          Specie s = (Specie) itr.next();
          printFormatter.format("%1$10s, %2$10d, %3$10.4f%4$1%%5$n", s.getName(), s
              .getAccumulatorValue(), Float
              .valueOf(100F * (Float.valueOf(s.getAccumulatorValue()) / this.surfacePopulation
                  .buriedTotalSize())));
          loggerFormatter.format(" %1$10s, %2$10E, %3$7.4f%4$1% ", s.getName(), s
              .getAccumulatorValue().doubleValue(), Float.valueOf(100F * (Float.valueOf(s
              .getAccumulatorValue()) / this.surfacePopulation.buriedTotalSize())));
        }

        sb1.append(printFormatter + spacer + "----" + newLine);
        sb2.append(loggerFormatter);

        // extract helper population statistics
        printFormatter = new Formatter();
        loggerFormatter = new Formatter();
        logstr = "helper population species: "
            + this.surfacePopulationHelper.getPopulationSpeciesNum() + ", total population: "
            + this.surfacePopulationHelper.getPopulationTotalSize();
        sb1.append(spacer + logstr + newLine);
        sb2.append(logstr);

        itr = this.surfacePopulationHelper.iterator();
        while (itr.hasNext()) {
          Specie s = (Specie) itr.next();
          printFormatter.format("%1$10s, %2$10d%5$n", s.getName(), s.getAccumulatorValue());
          loggerFormatter.format(" %1$10s, %2$10E,", s.getName(), s.getAccumulatorValue()
              .doubleValue());
        }
        sb1.append(printFormatter + spacer + "----" + newLine);
        sb2.append(loggerFormatter);

        // notify observers with chemistry done joy and log the statistics
        setChanged();
        notifyObservers(sb1.toString());
        logMessage(sb1.toString());
        this.cloudLogger.finer(sb2.toString());

        if (this.isStopSimulation) {
          this.cloudLogger.finer("Simulation stopped by request.");
          break;
        }

        StringBuilder logmessage = getStatistics(this.reactionLoggerStorage);
        logMessage("## final routes statistics:\n" + logmessage.toString() + spacer + "----"
            + newLine);

        logmessage = getCPStatistics(this.coreactantPickingStorage, "picks");
        logMessage("## final picks statistics:\n" + logmessage.toString() + spacer + "----"
            + newLine);

        logmessage = getCPStatistics(this.summaryLog, "other");
        logMessage("## final auxiliary statistics:\n" + logmessage.toString() + spacer + "----"
            + newLine);

        // #######################################

        ArrayList<String> species = new ArrayList<String>();

        for (String st : this.surfacePopulation.totalGetSpeciesNames()) {
          if (species.contains(st)) {
            assert true;
          }
          else {
            species.add(st);
          }
        }

        for (String st : this.reactionLoggerStorage.keySet()) {
          if (species.contains(st)) {
            assert true;
          }
          else {
            species.add(st);
          }
        }

        for (String st : this.coreactantPickingStorage.keySet()) {
          if (species.contains(st)) {
            assert true;
          }
          else {
            species.add(st);
          }
        }

        logmessage = new StringBuilder(10000);
        Collections.sort(species);
        for (String st : species) {

          Formatter cf = new Formatter();
          cf.format("%1$15s", st);

          if (this.surfacePopulation.totalContains(st)) {
            cf.format("%1$15d%2$15.4f", this.surfacePopulation.totalSpecieVolume(st),
                this.surfacePopulation.totalSpecieVolume(st)
                    / this.surfacePopulation.totalTotalSize().doubleValue());
          }
          else {
            cf.format("%1$15d%2$15.4f", 0, 0D);
          }

          // coreactant picking statistics
          if (this.coreactantPickingStorage.containsKey(st)) {
            cf.format("%1$15d", this.coreactantPickingStorage.get(st).getValue());
          }
          else {
            cf.format("%1$15d", 0L);
          }

          // reactant picking statistics
          if (this.reactantPickingStorage.containsKey(st)) {
            cf.format("%1$15d%2$n", this.reactantPickingStorage.get(st).getValue());
          }
          else {
            cf.format("%1$15d%2$n", 0L);
          }

          logmessage.append(cf);

        }

        Long hO = 0L;
        if ((null != this.reactionLoggerStorage.get("H"))
            && (null != this.reactionLoggerStorage.get("H").get("O"))) {
          hO = this.reactionLoggerStorage.get("H").get("O").getValue();
          if (null == hO) {
            hO = 0L;
          }
        }

        Long hO3 = 0L;
        if ((null != this.reactionLoggerStorage.get("H"))
            && (null != this.reactionLoggerStorage.get("H").get("O3"))) {
          hO3 = this.reactionLoggerStorage.get("H").get("O3").getValue();
          if (null == hO3) {
            hO3 = 0L;
          }
        }

        Long dO = 0L;
        if ((null != this.reactionLoggerStorage.get("D"))
            && (null != this.reactionLoggerStorage.get("D").get("O"))) {
          dO = this.reactionLoggerStorage.get("D").get("O").getValue();
          if (null == dO) {
            dO = 0L;
          }
        }

        Long hEv = 0L;
        Accumulator<Long> hEvAcc = this.summaryLog.get("H_evaporated");
        if (null != hEvAcc) {
          hEv = hEvAcc.getValue();
        }

        logMessage("## final formatted statistics:\n" + "Density = "
            + decFormatE.format(this.cloudDensity) + " : N = "
            + decFormatE.format(this.accretionStep) + "\nH+O3=" + hO3 + " H+O=" + hO + " D+O=" + dO
            + " H_evap=" + hEv
            + "\n         Species        SurfCon       FracAbun        CoReact          React\n"
            + logmessage.toString());

      }

    }
    catch (SecurityException e) {
      e.printStackTrace();
    }
    catch (IOException e) {
      e.printStackTrace();
    }
    catch (CloudReactionsException e) {
      e.printStackTrace();
    }

    tearDown();
  }

  /**
   * Does accretion step from gas phase to grain surface.
   *
   * @return the population of accreted species.
   */
  public PopulationSimple runCycleAccretion() {

    // Map that holds all species to accrete. The interval is mapped to certain specie.
    TreeSet<Interval> species = new TreeSet<Interval>(new IntervalComparator());

    // Map that "accumulates" species. Maps accreted volume to specie name.
    TreeMap<String, Specie> accretionSample = new TreeMap<String, Specie>();

    // initialize all intervals according the rates
    Double currentLow = 0.0;

    for (Specie sp : this.cloudPopulation) {
      if (sp.getAccretionProbability().equals(0D)) {
        assert true; // skipping this one
      }
      else {
        Double currentHigh = currentLow + sp.getAccretionProbability();
        species.add(new Interval(sp.getName(), currentLow, currentHigh));
        currentLow = currentHigh;
        accretionSample.put(sp.getName(), sp.clone());
        accretionSample.get(sp.getName()).setAccumulatorValue(0L);
      }
    }

    // *********************************** do a little logging
    StringBuilder logMessage = new StringBuilder(2048);
    Formatter frmt = new Formatter();
    frmt.format("%1$s ", "accretion_intervals");
    for (Interval intr : species) {
      frmt.format("%1$s %2$4f %3$4f, ", intr.getName(), intr.getLowBound(), intr.getHighBound());
    }
    logMessage.append(frmt).delete(logMessage.length() - 2, logMessage.length());
    this.cloudLogger.finer(logMessage.toString());
    // ***********************************

    // accrete all guys
    Interval[] iArray = new Interval[species.size()];
    iArray = species.toArray(iArray);

    for (int i = 0; i < this.accretionStep; i++) {
      Double rnd = this.random.nextDouble();
      String sp = getHit(rnd, iArray);
      accretionSample.get(sp).incrementAccumulatorValue(1L);
    }

    return new PopulationSimple(accretionSample);
  }

  /**
   * Gets over reaction network with provided accreted species.
   *
   * @param newComers newly accreted species.
   * @throws CloudReactionsException if error encountered.
   */
  public void runSurfaceCycle(PopulationSimple newComers) throws CloudReactionsException {

    // cleanUp newComers, if some of the cloud population species
    // found not accreted - remove them from the newcomers at all
    // I know the code looks weird, but i had hard time with synchronization
    boolean inLoop = true;
    while (inLoop) {
      inLoop = false;
      for (Specie sp : newComers) {
        if (sp.getAccumulatorValue().equals(0L)) {
          newComers.removeSpecie(sp.getName());
          inLoop = true;
          break;
        }
      }
    }

    this.cloudLogger.fine("Intermediate:" + newComers.getLogMessage().toString());

    // main cycle flag
    inLoop = true;

    // Map that holds all species to be accreted.
    // Each of the intervals is mapped to the certain specie.
    TreeMap<String, Interval> chosenIntervals = new TreeMap<String, Interval>();

    // Holds all reactions that are possible for the certain specie.
    // this map will be updated at the each step taking in account current surface population
    TreeMap<String, Reaction> possibleReactions = new TreeMap<String, Reaction>();

    // Holds all coreactants that are possible for the certain specie.
    // this map will be updated at the each step taking in account current surface population
    TreeMap<String, Reaction> coreactants = new TreeMap<String, Reaction>();

    // this storage is used to temporarily hold species that are
    // product of current (just finished) reaction
    ArrayList<Specie> products = new ArrayList<Specie>();

    // next two variables used to track down some logging information
    Integer accretionEventsCounter = 0;
    Integer lastLoggedValue = 0;
    String tempR = null;

    // ########## that flag is extremely crucial for burying stuff
    boolean justAdd = false;

    while (inLoop) {

      Specie choosenBeing = null;

      try {

        // ****************************************************************
        // this block solely treat the "leftovers" from previous reaction
        // ****************************************************************
        if (products.size() > 0) {

          this.cloudLogger.fine("case_2: accretion event: " + accretionEventsCounter + " got "
              + products.get(0).getName() + " as the product of " + tempR + ", grain popSize: "
              + this.surfacePopulation.totalTotalSize() + " details: "
              + this.surfacePopulation.getLogMessage().toString());

          // if there is more than one leftover in here - STOP IT!
          if (products.size() > 1) {
            StringBuilder sb = new StringBuilder(500);
            sb.append("got in trouble with excessive products" + " " + list(products) + " "
                + "reaction:" + tempR);
            throw new CloudReactionsException(sb.toString());
          }

          // fix the "chosen being" - i.e. one that will react
          choosenBeing = products.get(0).clone();
          products.remove(0);
          justAdd = false;

          // now we have secured the product specie from the
          // previous reaction, let's find what we can do with it
          if ("OH".equalsIgnoreCase(choosenBeing.getName())) {
            // checking whether to go to H2 or CO, barrier_CO=theta_species[co_ind]/Nsites
            // OK, here we will make CO or H2O depending on probability,

            // initial fixture to make life easier
            Double rnd = 0D;
            Double probabilityH2 = 1.0;
            Double probabilityCO = 0.0;

            if (this.surfacePopulation.surfaceContains("CO")) {
              // p_co=1-(1-barrier_CO)^4
              Double barrierCO = 1D
                  - this.surfacePopulation.surfaceGetSpecie("CO").getAccumulatorValue()
                      .doubleValue() / this.activeSites.doubleValue();
              probabilityCO = barrierCO * barrierCO;
              probabilityCO = 1 - probabilityCO * probabilityCO;
              probabilityH2 = 1 - probabilityCO;

              // probability normalization
              Double totalP = probabilityH2 + probabilityCO;
              probabilityCO = probabilityCO / totalP;
              probabilityH2 = probabilityH2 / totalP;

              rnd = this.random.nextDouble();
            } // just above we have corrected probabilities and random number in
            // the case if CO was found within the surface

            // now lets figure out who win H2 or CO.
            if (rnd < probabilityH2) {
              // got H2 reacted!
              tempR = "OH_H2";

              choosenBeing = new Molecule("H2O", 1L);
              justAdd = false;

              // REMEMBER: CASCADE STUFF
              if (this.cascadeIsOn) {
                products.add(new Atom("H", 1L));
                logSpeciePick(this.summaryLog, "H_released");
              }

              logReaction(this.reactionLoggerStorage, "OH", "H2");
              logSpeciePick(this.reactantPickingStorage, "OH");
              logSpeciePick(this.coreactantPickingStorage, "H2");

              logSpeciePick(this.summaryLog, "H2_picked");
              logSpeciePick(this.summaryLog, "H2O_produced");
            }
            else {
              // got CO reacted!
              // do some logging
              tempR = "OH_CO";
              this.cloudLogger.finer("got CO reacted: p_H2 "
                  + probabilityH2
                  + " p_CO "
                  + probabilityCO
                  + " numCO_onSurf: "
                  + this.surfacePopulation.surfaceGetSpecie("CO").getAccumulatorValue()
                      .doubleValue());
              // remove it from the surface first and add products
              this.surfacePopulation.surfaceDecrementSpecie("CO");
              choosenBeing = new Molecule("CO2", 1L);
              justAdd = true;

              // REMEMBER: CASCADE STUFF
              if (this.cascadeIsOn) {
                products.add(new Atom("H", 1L));
                logSpeciePick(this.summaryLog, "H_released");
              }

              logReaction(this.reactionLoggerStorage, "OH", "CO");
              logSpeciePick(this.reactantPickingStorage, "OH");
              logSpeciePick(this.coreactantPickingStorage, "CO");

              logSpeciePick(this.summaryLog, "CO_picked");
              logSpeciePick(this.summaryLog, "CO2_produced");
            }
          }
          else if ("OD".equalsIgnoreCase(choosenBeing.getName())) {

            // checking whether to go to H2 or CO
            // barrier_CO=theta_species[co_ind]/Nsites
            // making fixture again
            Double rnd = 0D;
            Double probabilityH2 = 1.0;
            Double probabilityCO = 0.0;

            if (this.surfacePopulation.surfaceContains("CO")) {
              // p_co=1-(1-barrier_CO)^4
              Double barrierCO = 1D
                  - this.surfacePopulation.surfaceGetSpecie("CO").getAccumulatorValue()
                      .doubleValue() / this.activeSites.doubleValue();
              probabilityCO = barrierCO * barrierCO;
              probabilityCO = probabilityCO * probabilityCO;

              probabilityCO = 1 - probabilityCO;
              probabilityH2 = 1 - probabilityCO;

              Double totalP = probabilityH2 + probabilityCO;
              probabilityH2 = probabilityH2 / totalP;
              probabilityCO = probabilityCO / totalP;
              // now lets figure out who won.
              rnd = this.random.nextDouble();
            } // just above we have corrected probabilities and random number in
            // the case if CO was found within the surface

            if (rnd < probabilityH2) {
              tempR = "OD_H2";
              choosenBeing = new Molecule("HDO", 1L);
              justAdd = false;

              // REMEMBER: CASCADE STUFF
              if (this.cascadeIsOn) {
                products.add(new Atom("H", 1L));
                logSpeciePick(this.summaryLog, "H_released");
              }

              logReaction(this.reactionLoggerStorage, "OD", "H2");
              logSpeciePick(this.reactantPickingStorage, "OD");
              logSpeciePick(this.coreactantPickingStorage, "H2");

              logSpeciePick(this.summaryLog, "H2_picked");
              logSpeciePick(this.summaryLog, "H_released");
              logSpeciePick(this.summaryLog, "HDO_produced");
            }
            else {
              // got CO reacted!
              // do some logging
              tempR = "OD_CO";
              this.cloudLogger.finer("got CO reacted: p_H2 "
                  + probabilityH2
                  + " p_CO "
                  + probabilityCO
                  + " numCO_onSurf: "
                  + this.surfacePopulation.surfaceGetSpecie("CO").getAccumulatorValue()
                      .doubleValue());
              // remove it from the surface first and add products
              this.surfacePopulation.surfaceDecrementSpecie("CO");
              choosenBeing = new Molecule("CO2", 1L);

              justAdd = true;
              products.add(new Atom("D", 1L));

              // do some logging
              logReaction(this.reactionLoggerStorage, "OD", "CO");
              logSpeciePick(this.reactantPickingStorage, "OD");
              logSpeciePick(this.coreactantPickingStorage, "CO");

              logSpeciePick(this.summaryLog, "CO_picked");
              logSpeciePick(this.summaryLog, "D_released");
              logSpeciePick(this.summaryLog, "CO2_produced");
            }
          }
          // ****************************************************************
          // OH and OD leftovers treatment ends right there.
          // ****************************************************************
          // note that if not OH was in products leftovers it is now chosen being!!!
          // ****************************************************************
        }
        else {
          // ****************************************************************
          // this section plays when there was no leftovers - we are accreting
          // i.e. products size == 0.
          // ****************************************************************
          chosenIntervals = refreshIntervals(chosenIntervals, newComers);
          Double rnd = this.random.nextDouble();
          for (String sp : chosenIntervals.keySet()) {
            if (chosenIntervals.get(sp).within(rnd)) {
              choosenBeing = newComers.getSpecie(sp);
              break;
            }
          }
          justAdd = false;
          newComers.decrementSpecieAccumulator(choosenBeing.getName(), 1L);
          logSpeciePick(this.summaryLog, choosenBeing.getName() + "_taken_from_gas");
          accretionEventsCounter++;
        }

        String choosenBeingName = choosenBeing.getName();

        // ****************************************************************
        // NOW.
        // we have selected specie that suppose to rule all following part of this method.
        // basically we are filtering all reactions with choosenBeing at first and all
        // reactions that are possible due to the presence of co-reactants at second step treated
        // ****************************************************************

        possibleReactions = getPossibleReactions(choosenBeingName, possibleReactions);

        if (possibleReactions.isEmpty()) {
          // in this case there is nothing to react with, so keep or evaporate this one

          if ("H".equalsIgnoreCase(choosenBeingName)) {
            // if hydrogen is accreted - remove one from the pool
            assert true; // i.e just ignore this - evaporated
            logSpeciePick(this.summaryLog, "H_evaporated");
          }
          else {
            // in this case not hydrogen accreted - put the specie within the surface bins
            this.cloudLogger.finer(this.surfacePopulation.addSpecie(choosenBeingName, justAdd));
            logSpeciePick(this.summaryLog, choosenBeingName + "_stuck_on_surface");
          }
        } // if possibleReactions == 0;

        else {
          // possible reactions are not zero - go for chemistry there
          coreactants.clear();

          if ("H".equalsIgnoreCase(choosenBeingName)) {

            for (String coreactant : possibleReactions.keySet()) {
              coreactants.put(coreactant, possibleReactions.get(coreactant));
            }

            Interval[] intervals = getSurfaceIntervals(coreactants);

            // find coreactant
            Double rnd = this.random.nextDouble();
            String coreactant = getHit(rnd, intervals);
            Reaction choosenReaction = possibleReactions.get(coreactant);

            tempR = choosenReaction.getName();
            // ############### do a little logging here
            logSpeciePick(this.summaryLog, "H_reacted");
            logReaction(this.reactionLoggerStorage, choosenBeingName, coreactant);
            logSpeciePick(this.reactantPickingStorage, choosenBeingName);
            logSpeciePick(this.coreactantPickingStorage, coreactant);
            // ###############

            // remove coreactant from grain
            this.surfacePopulation.surfaceDecrementSpecie(coreactant);

            // investigating the products list right there
            // add products to the surface or in the products list
            ArrayList<String> productSpecies = choosenReaction.getProducts();

            for (String productName : productSpecies) {
              if (("OH".equalsIgnoreCase(productName)) || ("OD".equalsIgnoreCase(productName))) {
                // OH and OD kept for the specific treatment within the products leftovers
                products.add(new Molecule(productName, 1L));
              }
              else if ("H2".equalsIgnoreCase(productName)) {
                // do nothing here. H2 released
                assert true;
              }
              else if (("HD".equalsIgnoreCase(productName)) || ("D2".equalsIgnoreCase(productName))) {
                // by agreement we keep HD and D2 in the separate bin.
                this.cloudLogger.finer(this.surfacePopulationHelper.addProduct(productName, 1L));
              }
              else if ("H".equalsIgnoreCase(productName)) {
                // in the case H was released during reactions - it kept into products leftovers
                // in fact it shouldn't happens
                products.add(new Atom(productName, 1L));
              }
              // all others common cases
              // i.e. product is not OH, OD, H2 or H
              else {
                this.cloudLogger.finer(this.surfacePopulation.addSpecie(productName, true));
              }
            } // for loop over the produced products

          }
          else {
            // here goes something that not H - different algorithm for interval
            // collect all possible coreactants and their abundances.
            for (String coreactant : possibleReactions.keySet()) {
              coreactants.put(coreactant, possibleReactions.get(coreactant));
            }
            Interval[] intervals = getSurfaceIntervals(coreactants);

            // find coreactant
            Double rnd = this.random.nextDouble();
            String coreactant = getHit(rnd, intervals);
            Reaction choosenReaction = possibleReactions.get(coreactant);

            tempR = choosenReaction.getName();
            // ############### do a little logging here
            logSpeciePick(this.summaryLog, choosenBeingName + "_reacted");
            logReaction(this.reactionLoggerStorage, choosenBeingName, coreactant);
            logSpeciePick(this.reactantPickingStorage, choosenBeingName);
            logSpeciePick(this.coreactantPickingStorage, coreactant);
            // *********************

            // remove coreactant from grain
            this.surfacePopulation.surfaceDecrementSpecie(coreactant);

            // add products to the surface or in the products list
            ArrayList<String> productSpecies = choosenReaction.getProducts();
            for (String productName : productSpecies) {
              if (("OH".equalsIgnoreCase(productName)) || ("OD".equalsIgnoreCase(productName))) {
                products.add(new Molecule(productName, 1L));
              }
              else if (("HD".equalsIgnoreCase(productName)) || ("D2".equalsIgnoreCase(productName))) {
                // by agreement we keep HD and D2 in the separate bin.
                this.cloudLogger.finer(this.surfacePopulationHelper.addProduct(productName, 1L));
              }
              else if ("H".equalsIgnoreCase(productName)) {
                products.add(new Atom(productName, 1L));
              }

              else {
                // i.e. product is not OH, OD, H2 or H
                this.cloudLogger.finer(this.surfacePopulation.addSpecie(productName, true));
              }
            } // for
          }
        } // possible reactions are not empty
      }
      catch (PopulationException e) {
        e.printStackTrace();
        break;
      }
      if ((zeroValue.equals(newComers.getPopulationSpeciesNum())) && (products.isEmpty())) {
        inLoop = false;
      }

      Long grainPSize = this.surfacePopulation.totalTotalSize();
      if ((!lastLoggedValue.equals(accretionEventsCounter))
          && (zeroValue.equals(accretionEventsCounter % 5000))) {
        this.cloudLogger.fine("case_1: accretion event: " + accretionEventsCounter
            + ", grain popSize: " + grainPSize + " details: "
            + this.surfacePopulation.getLogMessage().toString());
        lastLoggedValue = accretionEventsCounter;
      }

    } // while (inLoop)

  }

  /**
   * Sets the logger level for simulation.
   *
   * @param logLevel the level to set.
   */
  private void setLogLevel(String logLevel) {
    if ("severe".equalsIgnoreCase(logLevel)) {
      this.logLevel = Level.SEVERE;
    }
    else if ("fine".equalsIgnoreCase(logLevel)) {
      this.logLevel = Level.FINE;
    }
    else if ("finer".equalsIgnoreCase(logLevel)) {
      this.logLevel = Level.FINER;
    }
    else if ("finest".equalsIgnoreCase(logLevel)) {
      this.logLevel = Level.FINEST;
    }
  }

  /**
   * Sets the physical parameters for this cloud.
   *
   * @param parametersMessage the message containing the set of parameters.
   *
   * @throws ReadConfigException when an error found.
   */
  public void setParameters(CloudParametersMessage parametersMessage) throws ReadConfigException {

    if (parametersMessage.contains(CloudParametersMessage.CFGNAME)) {
      this.configFileName = parametersMessage.get(CloudParametersMessage.CFGNAME);
      loadConfig();
    }

    for (String parameter : parametersMessage.getParameters().keySet()) {
      if (parameter.equalsIgnoreCase(CloudParametersMessage.TEMPERATURE)) {
        this.cloudTemperature = Double.valueOf(parametersMessage
            .get(CloudParametersMessage.TEMPERATURE));
      }
      else if (parameter.equalsIgnoreCase(CloudParametersMessage.DENSITY)) {
        this.cloudDensity = Double.valueOf(parametersMessage.get(CloudParametersMessage.DENSITY));
      }
      else if (parameter.equalsIgnoreCase(CloudParametersMessage.SITESNUM)) {
        this.activeSites = Long.valueOf(parametersMessage.get(CloudParametersMessage.SITESNUM));
      }
      else if (parameter.equalsIgnoreCase(CloudParametersMessage.GRAINSIZE)) {
        this.cloudGrainSize = Double.valueOf(parametersMessage
            .get(CloudParametersMessage.GRAINSIZE));
      }
      else if (parameter.equalsIgnoreCase(CloudParametersMessage.ITRLIMIT)) {
        this.iterationsLimit = Integer.valueOf(parametersMessage
            .get(CloudParametersMessage.ITRLIMIT));
      }
      else if (parameter.equalsIgnoreCase(CloudParametersMessage.ACCSTEP)) {
        this.accretionStep = Double.valueOf(parametersMessage.get(CloudParametersMessage.ACCSTEP))
            .intValue();
      }
      else if (parameter.equalsIgnoreCase(CloudParametersMessage.LOGNAME)) {
        this.logName = parametersMessage.get(CloudParametersMessage.LOGNAME);
      }
      else if (parameter.equalsIgnoreCase(CloudParametersMessage.LOGGERNAME)) {
        this.loggerName = parametersMessage.get(CloudParametersMessage.LOGGERNAME);
      }
      else if (parameter.equalsIgnoreCase(CloudParametersMessage.LOGGERTYPE)) {
        this.logFormat = parametersMessage.get(CloudParametersMessage.LOGGERTYPE);
      }
      else if (parameter.equalsIgnoreCase(CloudParametersMessage.LOGGERLEVEL)) {
        setLogLevel(parametersMessage.get(CloudParametersMessage.LOGGERLEVEL));
      }
      else if (parameter.equalsIgnoreCase(CloudParametersMessage.DHRATIO)) {
        this.DHRatio = Double.valueOf(parametersMessage.get(CloudParametersMessage.DHRATIO));
      }
      else if (parameter.equalsIgnoreCase(CloudParametersMessage.BURY)) {
        this.buryIsOn = Boolean.valueOf(parametersMessage.get(CloudParametersMessage.BURY));
      }
      else if (parameter.equalsIgnoreCase(CloudParametersMessage.CASCADE)) {
        this.cascadeIsOn = Boolean.valueOf(parametersMessage.get(CloudParametersMessage.CASCADE));
      }
      else if (parameter.equalsIgnoreCase(CloudParametersMessage.CFGNAME)) {
        assert true;
      }
      else {
        throw new ReadConfigException("Unknown parameter " + parameter);
      }
    }
    initialize();
  }

  /**
   * Reports parameters that are used in cloud setup.
   *
   * @return the parameters message that reports all parameters used in cloud setup.
   */
  public CloudParametersMessage getCloudParameters() {
    CloudParametersMessage res = new CloudParametersMessage();
    try {
      res.addParameter(CloudParametersMessage.TEMPERATURE, this.cloudTemperature.toString());
      res.addParameter(CloudParametersMessage.DENSITY, this.cloudDensity.toString());
      res.addParameter(CloudParametersMessage.SITESNUM, this.activeSites.toString());
      res.addParameter(CloudParametersMessage.GRAINSIZE, this.cloudGrainSize.toString());
      res.addParameter(CloudParametersMessage.DHRATIO, this.DHRatio.toString());
      res.addParameter(CloudParametersMessage.BURY, this.buryIsOn.toString());
      res.addParameter(CloudParametersMessage.CASCADE, this.cascadeIsOn.toString());
      res.addParameter(CloudParametersMessage.ITRLIMIT, this.iterationsLimit.toString());
      res.addParameter(CloudParametersMessage.ACCSTEP, this.accretionStep.toString());
      res.addParameter(CloudParametersMessage.CFGNAME, this.configFileName);
      res.addParameter(CloudParametersMessage.LOGNAME, this.logName);
      res.addParameter(CloudParametersMessage.LOGGERNAME, this.loggerName);
      res.addParameter(CloudParametersMessage.LOGGERTYPE, this.logFormat);
      res.addParameter(CloudParametersMessage.LOGGERLEVEL, this.logLevel.toString());
    }
    catch (ReadConfigException e) {
      e.printStackTrace();
    }
    return res;
  }

  /**
   * Sets the surface population.
   *
   * @param surfacePopulation the population to set.
   */

  public void setSurfacePopulation(PopulationWithBury surfacePopulation) {
    this.surfacePopulation = surfacePopulation;
  }

  /**
   * ShutDowns the logging process.
   *
   */
  public void tearDown() {
    if (null != this.logWriter) {
      try {
        this.logWriter.close();
      }
      catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }
  }

  /**
   * Updates the state of stop flag.
   *
   * @param o object to observe.
   * @param arg message from the object.
   */
  public void update(Observable o, Object arg) {
    if ((o instanceof Controller) && (arg instanceof Boolean)) {
      this.isStopSimulation = (Boolean) arg;
    }
  }

  /**
   * Updates all accretion probabilities.
   */
  public void updateAccretionRates() {
    // Original code looks like:
    // n_abundance = (10.^iterate)*1L
    // n_O2 = (f_O2*(A_O-(ABS((1-f_C)*A_C)))*n_abundance)*mo2 ;abundance of molecular oxygen
    // n_O = (1-2*f_O2)*(ABS(A_O-((1-f_C)*A_C)))*n_abundance ;abundance of oxygen
    // n_N = (A_N*f_N)*n_abundance
    // n_N2 = (((1-f_N)*A_N)/2)*n_abundance
    // n_C = (A_C*f_C)*n_abundance
    // n_CO = ((1-f_C)*A_C)*n_abundance ;abundance of carbon monoxide

    for (Specie specie : this.cloudPopulation) {
      if ("H".equals(specie.getName())) {
        Atom hydrogen = (Atom) this.cloudPopulation.getSpecie("H");
        hydrogen.setAccretionProbability(1.0); // always 1.0
      }
      else if ("D".equals(specie.getName())) {
        Atom deiterium = (Atom) this.cloudPopulation.getSpecie("D");
        Double ap = deiterium.getFraction() * deiterium.getAbundance();
        // ap = (ap * this.cloudPopulation.get("H").getVelocity()) / deiterium.getVelocity();
        deiterium.setAccretionProbability(ap);
      }
      else if ("O".equals(specie.getName())) {
        // n_O = (1-2*f_O2)*(ABS(A_O-((1-f_C)*A_C)))*n_abundance ;abundance of oxygen
        Atom carbon = (Atom) this.cloudPopulation.getSpecie("C");
        Atom oxygene = (Atom) this.cloudPopulation.getSpecie("O");
        Molecule oxygeneM = (Molecule) this.cloudPopulation.getSpecie("O2");
        Double ap = (1 - oxygeneM.getFraction() * 2)
            * Math.abs(oxygene.getAbundance() - (1 - carbon.getFraction()) * carbon.getAbundance())
            * this.cloudDensity;
        oxygene.setAccretionProbability(ap);
      }
      else if ("O2".equals(specie.getName())) {
        // n_O2 = (f_O2*(A_O-(ABS((1-f_C)*A_C)))*n_abundance)*mo2 ;abundance of molecular oxygen
        Atom carbon = (Atom) this.cloudPopulation.getSpecie("C");
        Atom oxygene = (Atom) this.cloudPopulation.getSpecie("O");
        Molecule oxygeneM = (Molecule) this.cloudPopulation.getSpecie("O2");
        Double ap = (oxygeneM.getFraction() * (oxygene.getAbundance() - Math.abs(1 - carbon
            .getFraction())
            * carbon.getAbundance()))
            * this.cloudDensity;
        oxygeneM.setAccretionProbability(ap);
      }
      else if ("N".equals(specie.getName())) {
        // n_N = (A_N*f_N)*n_abundance
        Atom nitrogene = (Atom) this.cloudPopulation.getSpecie("N");
        Double ap = (nitrogene.getAbundance() * nitrogene.getFraction()) * this.cloudDensity;
        nitrogene.setAccretionProbability(ap);
      }
      else if ("N2".equals(specie.getName())) {
        // n_N2 = (((1-f_N)*A_N)/2)*n_abundance
        Molecule nitrogeneM = (Molecule) this.cloudPopulation.getSpecie("N2");
        Atom nitrogene = (Atom) this.cloudPopulation.getSpecie("N");
        Double ap = ((1 - nitrogene.getFraction()) * nitrogene.getAbundance() / 2)
            * this.cloudDensity;
        nitrogeneM.setAccretionProbability(ap);
      }
      else if ("C".equals(specie.getName())) {
        // n_C = (A_C*f_C)*n_abundance
        Atom carbon = (Atom) this.cloudPopulation.getSpecie("C");
        Double ap = (carbon.getAbundance() * carbon.getFraction()) * this.cloudDensity;
        carbon.setAccretionProbability(ap);
      }
      else if ("CO".equals(specie.getName())) {
        // n_CO = ((1-f_C)*A_C)*n_abundance ;abundance of carbon monoxide
        Atom carbon = (Atom) this.cloudPopulation.getSpecie("C");
        Molecule carbonOxide = (Molecule) this.cloudPopulation.getSpecie("CO");
        Double ap = (1 - carbon.getFraction()) * carbon.getAbundance() * this.cloudDensity;
        carbonOxide.setAccretionProbability(ap);
      }
    }

    Double normalizer = 0.0;
    for (Specie sp : this.cloudPopulation) {
      // if ("D".equalsIgnoreCase(sp.getName())) {
      // normalizer = normalizer + sp.getAccretionProbability()
      // * this.cloudPopulation.getSpecie("H").getVelocity();
      // }
      // else {
      normalizer = normalizer + sp.getAccretionProbability() * sp.getVelocity();
      // }
    }
    for (Specie sp : this.cloudPopulation) {
      // if ("D".equalsIgnoreCase(sp.getName())) {
      // sp.setAccretionProbability((sp.getAccretionProbability() * this.cloudPopulation.getSpecie(
      // "H").getVelocity())
      // / normalizer);
      // }
      // else {
      sp.setAccretionProbability((sp.getAccretionProbability() * sp.getVelocity()) / normalizer);
      // }
    }
  }

  /**
   * Sets up all energy barriers for reactions.
   *
   */
  private void updateChiValues() {

    // Calculate the probability of overcoming an energy barrier
    for (Reaction r : this.cloudReactions.values()) {

      String type = r.getType();

      /*---------------------------------------------*/
      if ((type.equalsIgnoreCase("QT")) && (r.getReactants().get(0).equalsIgnoreCase("H"))) {
        // (type[j] EQ 'QT') : chi[j] = nu*exp(-((2*a/h_bar)*(sqrt(2.*mass_H*k*E[j])))
        r.setChi(Constants.verticalOscillation
            * Math.exp((-2 * this.cloudGeneralBarrierWidth / Constants.reducedPlank)
                * Math.sqrt(2 * Constants.hydrogeneMass * Constants.boltzmann * r.getEBarrier())));
      }
      /*---------------------------------------------*/
      if ((type.equalsIgnoreCase("QT")) && (r.getReactants().get(0).equalsIgnoreCase("D"))) {
        // (type[j] EQ 'QT') : chi[j] = nu*exp(-((2*a/h_bar)*(sqrt(2.*mass_H*k*E[j])))
        r.setChi(Constants.verticalOscillation
            * Math.exp((-2 * this.cloudGeneralBarrierWidth / Constants.reducedPlank)
                * Math.sqrt(2 * Constants.deuteriumMass * Constants.boltzmann * r.getEBarrier())));
      }
      /*---------------------------------------------*/
      else if ((type.equalsIgnoreCase("DF")) && (r.getReactants().get(0).equalsIgnoreCase("H"))) {
        // (type[j] EQ 'DF') AND (Reactant[j] EQ 'H'):$
        // chi[j] = nu*exp(-((2*a/h_bar)*(sqrt(2.*mass_H*k*E[j]))))
        r.setChi(Constants.verticalOscillation
            * Math.exp(-((2 * this.cloudGeneralBarrierWidth / Constants.reducedPlank) * Math.sqrt(2
                * Constants.hydrogeneMass * Constants.boltzmann * r.getEBarrier()))));
      }
      /*---------------------------------------------*/
      else if ((type.equalsIgnoreCase("DF")) && (r.getReactants().get(0).equalsIgnoreCase("D"))) {
        // (type[j] EQ 'DF') AND (Reactant[j] EQ 'H'):$
        // chi[j] = nu*exp(-((2*a/h_bar)*(sqrt(2.*mass_H*k*E[j]))))
        r.setChi(Constants.verticalOscillation
            * Math.exp(-((2 * this.cloudGeneralBarrierWidth / Constants.reducedPlank) * Math.sqrt(2
                * Constants.deuteriumMass * Constants.boltzmann * r.getEBarrier()))));
      }
      /*---------------------------------------------*/
      else if ((type.equalsIgnoreCase("DF_O")) || (type.equalsIgnoreCase("DF_C"))
          || (type.equalsIgnoreCase("DF_N"))) {
        // (type[j] EQ 'DF_O') AND (Reactant[j] EQ 'O'): chi[j] = nu*exp(-(E[j]/T))
        // (type[j] EQ 'DF_C') AND (Reactant[j] EQ 'CO'): chi[j] = nu*exp(-(E[j]/T))
        // (type[j] EQ 'DF_C') AND (Reactant[j] EQ 'C'): chi[j] = nu*exp(-(E[j]/T))
        // (type[j] EQ 'DF_N') AND (Reactant[j] EQ 'N'): chi[j] = nu*exp(-(E[j]/T))
        r.setChi(Constants.verticalOscillation
            * Math.exp(-(r.getEBarrier() / this.cloudTemperature)));
      }
      /*---------------------------------------------*/
      else if (type.equalsIgnoreCase("DF")) {
        r.setChi(Constants.verticalOscillation
            * Math.exp(-(r.getEBarrier() / this.cloudTemperature)));
      }

    }
  }

  /**
   * Returns current cloud snapshot in Plain text format.
   *
   */
  public void writeSnapshot() {

    if (null == this.logWriter) {
      assert true;
    }
    else {
      try {
        this.logWriter.write("Iterations left: " + this.iterationsLimit);
        this.logWriter.write("\n\nN sites:     " + this.activeSites);
        this.logWriter.write("\nDensity:     " + this.cloudDensity);
        this.logWriter.write("\nTemperature: " + this.cloudTemperature);
        this.logWriter.write("\ngrainSize:   " + this.cloudGrainSize);
        // this.fileWriter.write("\nActivation H2CO: " + String.valueOf(this.activation_H2CO));
        // this.fileWriter.write("\nactivation_H2CO_computed: " +
        // String.valueOf(this.activation_H2CO_computed));
        this.logWriter.write("\naccretion time computed : " + this.cloudAccretionTimeSpan);
        this.logWriter.write("\n\nCurrent Population: \n");
        for (Specie sp : this.cloudPopulation) {
          this.logWriter.write(sp.toText());
        }
        this.logWriter.write("\nReactions configured: \n");
        for (Reaction r : this.cloudReactions.values()) {
          this.logWriter.write("  Reaction: " + r.getName() + ", type: " + r.getType() + ", Chi: "
              + r.getChi() + ", rate: " + r.getRate() + ", Ebarrier: " + r.getEBarrier()
              + ", reactants: " + r.getReactants().toString() + ", products: "
              + r.getProducts().toString() + "\n");
        }
        this.logWriter.write("\nAccretion probabilities: \n");
        Double sum = 0.0;
        for (Specie sp : this.cloudPopulation) {
          this.logWriter.write("  atom: " + sp.getName() + ", accretion probability: "
              + sp.getAccretionProbability() + "\n");
          sum = sum + sp.getAccretionProbability();

        }
        this.logWriter.write("Total (sum) is " + sum + "\n");
        // this.fileWriter.write(" For CO_OH probability is: " + this.chiCOOH + "\n");
        // this.fileWriter.write(" For H2OH probability is: " + this.chiH2OH + "\n");
        this.logWriter.flush();
      }
      catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }

  }

}
